Skip to content

FE-1544: Rebase the token spreadsheet on the worksheet focus layer - #9418

Open
kube wants to merge 1 commit into
claude/fe-1560-command-registryfrom
claude/fe-1544-spreadsheet-focus-layer
Open

FE-1544: Rebase the token spreadsheet on the worksheet focus layer#9418
kube wants to merge 1 commit into
claude/fe-1560-command-registryfrom
claude/fe-1544-spreadsheet-focus-layer

Conversation

@kube

@kube kube commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Stacked on #9457. Rebase the token spreadsheet (place initial state, scenario forms) on the worksheet keyboard-flow layer from #9411, replacing its hand-rolled focus and key routing. This fixes two shipping defects: Tab was intercepted everywhere and swallowed at the last cell, trapping keyboard users, and row focus went through an unscoped document.querySelector, so a scenario form with several per-place grids could focus the wrong grid.

🔗 Related links

🔍 What does this change?

  • Spreadsheet delegates movement, tab order, and the row-number lane to useFocusStops: each grid is one Tab stop with a roving tabindex, arrows walk cells and the gutter, and Tab leaves the grid instead of being trapped. The ~250-line hand-rolled key router, the focusedCell/selectedRow state, and every setTimeout/document.querySelector focus call are deleted.
  • Row selection follows the layer's useRowSelection grammar: the row whose gutter holds focus renders selected; Delete there removes the row, and the index-keyed rows keep focus in the lane afterwards.
  • Cell clicks follow the layer's select-first grammar (useSelectFirstActivation): the first click selects a cell, a click on the selected cell opens its editor. This also gives mouse users an editing path, which did not exist before (editing was keyboard-only).
  • Editor focus hand-off (open, commit-and-advance, cancel) runs through flushSync plus the grid's own target map instead of timers and document-wide queries.
  • The scenario form wraps its per-place grids in FocusRoot/FocusStack, so arrows flow from one place's spreadsheet into the next.
  • The grid declares role="grid"/row/gridcell/rowheader per the layer's contract, and the uuid overlay is revealed by CSS :focus-within instead of focus state.
  • Intentional behaviour changes: Enter/typing on a cell still edits, but Escape on a non-editing cell no longer blurs it; the row highlight clears when the gutter loses focus; boolean t/f/1/0 shortcuts now ignore modifier chords (Cmd+T reaches the browser again); printable keys typed into a cell no longer leak to the editor's global single-key shortcuts.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • With a row-number cell focused, single-letter editor shortcuts (v/h/n/t) still reach the canvas, as before this PR. Settling that globally is part of the wider focus-layer adoption.

🐾 Next steps

  • FE-1545 (internal) adopts the layer in the left sidebar lists and search.

🛡 What tests cover this?

  • New spreadsheet.test.tsx: roving tabindex, arrow movement and the gutter lane, Tab not intercepted, edit open/commit/advance/cancel, phantom-row materialization, gutter delete with focus retention, boolean key handling, select-first clicks, and cross-grid flow inside a FocusStack.
  • Existing focus-flow.test.tsx covers the underlying layer contract.

❓ How to test this?

  1. yarn workspace @hashintel/petrinaut storybook, open Components / Spreadsheet.
  2. Tab into the grid (one stop), walk cells and the row-number lane with arrows, press Enter or type to edit, Enter to commit and advance, Delete on a row number to remove the row, and fill the bottom phantom row to add one.
  3. Tab again: focus leaves the grid.
  4. In the app, open a scenario's Initial State section with two coloured places and arrow from one grid into the next.

The grid's visuals are unchanged; the existing docs screenshots stay valid.

🤖 Generated with Claude Code

@kube kube self-assigned this Aug 28, 2026
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 31, 2026 12:53am
petrinaut Ready Ready Preview Aug 31, 2026 12:53am
petrinaut-docs Ready Ready Preview Aug 31, 2026 12:53am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 31, 2026 12:53am

Request Review

@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team type/eng > backend Owned by the @backend team labels Aug 28, 2026
@kube
kube marked this pull request as ready for review August 28, 2026 20:28
@cursor

cursor Bot commented Aug 28, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
This is a broad rewrite of spreadsheet keyboard and focus behavior with intentional UX deltas (Escape, row highlight, modifier keys); regressions could affect accessibility and multi-grid scenario editing, though coverage is added via new tests.

Overview
Token spreadsheets (place initial state and scenario per-place grids) now use the shared worksheet focus layer instead of custom keyboard routing. Each grid is one Tab stop with roving tabindex; arrow keys move between cells and the row-number gutter, and Tab is no longer trapped inside the grid.

The large hand-rolled key handler, local focusedCell/selectedRow state, and document.querySelector / setTimeout focus hacks are removed. Movement and tab order go through useFocusStops; row highlight and gutter Delete use useRowSelection; click-to-select then click-to-edit uses useSelectFirstActivation (mouse editing path that did not exist before). Editor open/commit/cancel uses flushSync and a per-cell target map.

Scenario Initial State wraps multiple place spreadsheets in FocusRoot / FocusStack so arrows can flow from one place’s grid into the next. The grid adds role="grid" / rowheader / gridcell, and UUID full-string overlays show via :focus-within instead of React focus state.

Docs and a changeset describe the new keyboard model. spreadsheet.test.tsx covers roving tabindex, gutter lane, Tab passthrough, editing, phantom rows, gutter delete, booleans, select-first clicks, and cross-grid flow.

Reviewed by Cursor Bugbot for commit 261d243. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0bc5b0b. Configure here.

tabIndex={
isReadOnly
? undefined
: tabIndexFor({ stopId, column: "gutter" })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale roving tab stop after shrink

Medium Severity

The roving tabIndex is never cleared when the remembered cell disappears. After a type edit drops a column, or parent data shrinks past the last focused row, tabIndexFor matches nothing and every cell stays at -1, so Tab can no longer enter the grid until a click resets memory.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0bc5b0b. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

1 participant